/*
 * TxObjectProxyFactory.java   0.0.1    30/ott/2009
 * 
 * Copyright (c) 2009 mentalsmash.org
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * The use of the Apache License does not indicate that this project is
 * affiliated with the Apache Software Foundation.
 */
package org.mentalsmash.tazio.transactions.model.impl;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.mentalsmash.tazio.IllegalArgumentException;
import org.mentalsmash.tazio.transactions.model.IllegalPropertyException;
import org.mentalsmash.tazio.transactions.model.InvalidTransactionalObjectException;
import org.mentalsmash.tazio.transactions.model.Property;
import org.mentalsmash.tazio.transactions.model.RootSet;
import org.mentalsmash.tazio.transactions.model.TransactionalObject;
import org.mentalsmash.tazio.transactions.model.identifiers.OID;
import org.mentalsmash.tazio.transactions.user.UserTransactionManager;
import org.mentalsmash.tazio.transactions.user.annotations.PropertyGetter;
import org.mentalsmash.tazio.transactions.user.annotations.PropertySetter;
import org.mentalsmash.tazio.transactions.user.annotations.Transactional;
import org.mentalsmash.tazio.transactions.util.Utility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;

/**
 * @author Andrea Reale
 * @version 0.0.1
 */
public class TxObjectWrapperFactoryImpl implements TxObjectWrapperFactory {

    private static Logger log = LoggerFactory
	    .getLogger(TxObjectWrapperFactoryImpl.class);

    private final UserTransactionManager tm;

    private final RootSet rs;

    @Inject
    public TxObjectWrapperFactoryImpl(final UserTransactionManager tm,
	    final RootSet rs) {
	this.tm = tm;
	this.rs = rs;
    }

    /*
     * (non-Javadoc)
     * 
     * @seeorg.mentalsmash.tazio.transactions.impl.TxObjectWrapperFactory#
     * createTxWrapper(java.io.Serializable)
     */
    public TxObjectWrapper createTxWrapper(final Serializable target) {
	final long version = TransactionalObject.INITIAL_VERSION;
	final OID objectId = OID.generateOID();
	final TxObjectWrapper proxy;

	synchronized (target) {
	    final Map<String, Property> properties = validateTargetObject(target);
	    // the next line should be fine since the target was already
	    // validated
	    // TODO: Optimize
	    final Class<?> txInterface = Utility
		    .findTransactionalInterfaces(target)[0];
	    proxy = new TxObjectWrapper(objectId, version, target, txInterface,
		    properties, this.tm, this.rs);
	}

	return proxy;
    }

    /**
     * Validates an object for its "proxability" and extracts a map of its
     * transactional properties by reading the method annotations of its
     * interfaces annotated with {@link Transactional}
     * 
     * @param target
     *            the object to proxy
     * @return a map of properties for the given <code>target</code>
     * @throws InvalidTransactionalObjectException
     *             if the format of the interface marked with the
     *             {@link Transactional} annotation is not valid
     */
    private Map<String, Property> validateTargetObject(final Serializable target) {
	final Class<?> everyTxInterfaces[] = Utility
		.findTransactionalInterfaces(target);
	final Class<?> txInterface;
	final Map<String, Property> properties = new HashMap<String, Property>();

	if (everyTxInterfaces == null) {
	    log.error("Target object {} does not implement any interface "
		    + "annotaed with @Transactional");
	} else if (everyTxInterfaces.length != 1) {
	    log.error("Target object {} implements more"
		    + " then one interface annotaed with"
		    + "@Transactional annotation");
	    throw new InvalidTransactionalObjectException(""
		    + "The target object " + target + " implements"
		    + " more then one interface annotated with the "
		    + "@Transactional annotation");
	}
	txInterface = everyTxInterfaces[0];

	fillInProperties(txInterface, properties);
	return properties;

    }

    /**
     * Fills in the map of properties for the object wrapped by this proxy
     * 
     * @throws InvalidTransactionalObjectException
     *             if the transactional interface presents something wrong
     */
    private void fillInProperties(final Class<?> iface,
	    final Map<String, Property> properties) {

	for (final Method method : iface.getMethods()) {
	    if (method.isAnnotationPresent(PropertyGetter.class)) {
		addProperty(iface, method, properties);
	    }
	}

    }

    /**
     * Adds a property to the map of properties given a getter method of the
     * transactional interface
     * 
     * @param getterMethod
     *            the method annotated with {@link PropertyGetter} in a
     *            transactional interface
     */
    private void addProperty(final Class<?> iface, final Method getterMethod,
	    final Map<String, Property> properties) {

	final PropertyGetter getterAnnotation = getterMethod
		.getAnnotation(PropertyGetter.class);
	final String propertyName;
	final Method setterMethod;
	if (getterAnnotation == null) {
	    throw new IllegalArgumentException(
		    "Invalid argument: method is not correctly annotated");
	}

	propertyName = getterAnnotation.propertyName();

	// check whether it is a duplicate entry
	if (properties.containsKey(propertyName)) {
	    log.error("Duplicate getter for property {} in interface {}",
		    propertyName, iface.getSimpleName());
	    throw new InvalidTransactionalObjectException(
		    "Duplicate getter for property " + propertyName
			    + " in interface " + iface.getSimpleName());
	}

	if (!getterAnnotation.readOnly()) { // if not readonly I need to find
	    // the corresponding setter method
	    setterMethod = findSetterMethod(iface, propertyName);
	    if (setterMethod == null) {
		log.error("No setter for the property named {} "
			+ "in the interface {}", propertyName, iface
			.getSimpleName());
		throw new InvalidTransactionalObjectException(
			"No valid Property setter " + "was found for property "
				+ propertyName + " in the interface "
				+ iface.getSimpleName());
	    }
	} else { // if the property is readonly
	    setterMethod = null;
	}

	try {
	    properties.put(propertyName, new Property(propertyName,
		    getterMethod, setterMethod));
	} catch (final IllegalPropertyException e) {
	    log.error(e.getMessage(), e);
	    throw new InvalidTransactionalObjectException(e);
	}

    }

    /**
     * Finds a method annotated with {@link PropertySetter} and having name
     * <code>propertyName</code> in the interface <code>iface</code> and returns
     * it
     * 
     * @param propertyName
     *            the name of the property as expressed in the annotation
     * @return a reference to the method
     * @throws InvalidTransactionalObjectException
     *             if there is more then one setter for a property named
     *             <code>propertyName</code>
     */
    private Method findSetterMethod(final Class<?> iface,
	    final String propertyName) {

	Method toReturn = null;
	boolean found = false;

	for (final Method method : iface.getMethods()) {
	    if (method.isAnnotationPresent(PropertySetter.class)) {
		final PropertySetter annotation = method
			.getAnnotation(PropertySetter.class);
		if (annotation.propertyName().equals(propertyName)) {
		    if (!found) {
			toReturn = method;
			found = true;
		    } else { // check that there is exactly one setter for a
			// property
			log.error(
				"More then one PropertySetter method for property named "
					+ "{} in interface {}", propertyName,
				iface.getSimpleName());
			throw new InvalidTransactionalObjectException(
				"More then one PropertySetter method for property named "
					+ propertyName + " in interface "
					+ iface.getSimpleName());
		    }
		}
	    }
	}

	return toReturn;

    }

    /**
     * A proxied object is considered valid by this method if it is not
     * <code>null</code>, it is not already an instance of
     * {@link TxObjectWrapper} , and one of its interfaces is annotated with the
     * {@link Transactional} annotation
     * 
     * @param obj
     *            the instance to check
     * @return true if the object is valid
     */
    @SuppressWarnings("unused")
    private boolean isValidWrappedObject(final Serializable obj) {
	final Class<?>[] txInterfaces;

	if (obj == null) {
	    return false;
	}
	if (obj instanceof TxObjectWrapper) {
	    return false;
	}

	txInterfaces = Utility.findTransactionalInterfaces(obj);
	if (txInterfaces.length != 1) {
	    return false;
	}

	return true;
    }

}
